Mass Assignment脆弱性
外部から入力された値をフィルタせずにそのまま入力値とすることで意図しないプロパティが外部から設定できてしまう問題。
概念としてはOWASP Top 10 2017の安全でないデシリアライゼーションの一例にも含まれる。
読みは「マスアサインメントぜいじゃくせい」。
かつてはRuby on Railsすらこの問題を持った実装が横行しており、これがストロングパラメータの実装の契機になった。
GitHub、Mass Assignment利用の脆弱性を突かれる
脆弱性のあるコード例
たとえば以下のようなUserクラスを考える
mail_authorizedはメール認証
code:User.php
class User
{
protected function __construct(
private ?UserId $id = null,
private ?string $name = null,
private ?bool $mail_authorized
) {}
public static function createFromArray(array $params): self
{
return new self(
id: isset($params'id') ? new UserId($params'id') : null,
name: $params'name',
mail_authorized: $params'mail_authorized' ?? false,
);
}
public static function findById(int $id): ?self
{
$row = db_select('users', 'id' => $id, 'id', 'name', 'mail_authorized');
if (empty($row) {
return null;
}
return static::createFromArray($row);
}
public function completeMailAuthorize(): void
{
$this->mail_authorized = true;
}
public function save(): bool
{
return db_upsert('users', $this->params) !== null;
}
}
User::findById()でデータベースから検索して取得できる
たとえばメール認証するページはこう
code:mail_authorize.php
<?php session_start();
$session_user_id = ($_SESSION'user_id' ?? null;
if ($session_user_id === null) {
throw new ForbiddenException();
}
assert_access_token();
$user = User::findById($session_user_id) or throw new ForbiddenException();
$token = filter_var($_GET'token' ?? '', FILTER_DEFAULT);
if (!check_mail_authorization($user, $token)) {
throw new ForbiddenException();
}
$this->completeMailAuthorize();
$user->save();
外部から値を受け取って保存しようとする雑なコード
code:update_user_profile.php
<?php session_start();
$session_user_id = ($_SESSION'user_id' ?? null;
if ($session_user_id === null) {
throw new ForbiddenException();
}
assert_access_token();
$user = User::createFromArray('id' => $session_user_id + $_POST); // ←!!
$user->save();
ここで$_POSTをそのままUser::createFromArray()に渡しちゃってるのがまずい
['id' => $session_user_id]で最低限IDの上書きできないようにしているが、これでは足りない
それすらしないと好き勝手に他のユーザーのアカウント情報を変更できてしまう
対策
フレームワークによって対策が異なる。
一般的な対策
入力値をきちんとチェックして入力する
code:php
assert_access_token();
$name = filter_var($_GET'name' ?? false);
if ($name === '' || $name === false) {
throw new BadRequestException();
}
$user = User::createFromArray([
'id' => $session_user_id,
'name' => $name,
]);
$user->save();
Laravel
フォームリクエストとModelの$fillableを併用するといいんじゃないでしょうか
バリデーション 9.x Laravel
Eloquentの準備 9.x Laravel
Webを検索すると$fillableよりも$guardedの方が簡単! という記事も出てきますが、セキュアコーディングの観点からは保存していいカラムを明示的に指定する方が事故りにくいのではないでしょうか
思考停止して安易に$fillableに追加すると運用が常態化するとまずいのは同じですけど…
頻繁にカラムの加除が発生しないような設計を心掛けたいものです
CakePHP
エンティティークラスの$_accesibleを使うといいんじゃないでしょうか
エンティティー - CakePHP Cookbook 4.x
あとSecurityコンポーネントが有効な場合はFormヘルパーを使っているとフォーム改竄検知の恩恵を受けられます
Form - CakePHP Cookbook 4.x
セキュリティ - CakePHP Cookbook 4.x
#セキュリティ